package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;







    //以下测试分布式锁的环境
    @Override
    public  void testLock() {
        String uuid = UUID.randomUUID().toString().replaceAll("-","");

        //给锁标记设置过期时间，当应用程序无法释放锁，那么，redis根据过期时间也会自动释放锁。避免死锁。
        //Boolean ifAbsent = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
        Boolean ifAbsent = this.tryLock("lock",uuid,30L);
        //int i = 1/0;
        //this.stringRedisTemplate.expire("lock",10, TimeUnit.SECONDS);
        if(ifAbsent){ //获取分布式锁成功
            //int i = 1/0;
            //执行业务逻辑
            // 查询Redis中的num值
            String value = (String)this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
                return ;
            }
            // 有值就转成成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

//            if(uuid.equals(this.stringRedisTemplate.opsForValue().get("lock"))){ //说明锁的值是自己的，才会释放锁。避免释放别人的锁。
//                //刚一判断成立，再去删除时，也可能删除别人的锁。为什么？因为if判断和删除锁代码不是原子的，可能被打断。
//                //业务逻辑执行后，需要解锁。
//                this.stringRedisTemplate.delete("lock"); //可能删除别人加的锁？
//            }
            //判断和删除锁 做成原子的。LUA脚本
            //String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            //DefaultRedisScript defaultRedisScript = new DefaultRedisScript<>(script, Long.class);
            //defaultRedisScript.setResultType(Long.class);
            //defaultRedisScript.setScriptText(script);
            //this.stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"),uuid); //局部原子性

            // 睡眠60s，锁过期时间30s。每隔20s自动续期
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 测试可重入性
            //this.testSubLock(uuid);

            this.unlock("lock",uuid);

        }else{ //获取分布式锁失败
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
            }
            testLock(); // 递归，自旋
        }

    }

    private void testSubLock(String uuid) {
        Boolean ifAbsent = this.tryLock("lock",uuid,300L);
        if(ifAbsent){
            log.info("测试分布式锁可重入性"+Thread.currentThread().getId()+","+Thread.currentThread().getName());
            this.unlock("lock",uuid);
        }
    }


    private Boolean tryLock(String lockName, String uuid, Long expire){
        String script = "if(redis.call('exists' , KEYS[1]) == 0 ) then" +
                " redis.call('hset' , KEYS[1] ,ARGV[1] , 1)" +
                " redis.call('expire' , KEYS[1] , ARGV[2])" +
                "  return 1" +
                " elseif(redis.call('hexists' , KEYS[1],ARGV[1])==1) then" +
                " redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1)" +
                "  return 1" +
                " else" +
                "  return 0" +
                " end";
        if (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
            try {
                // 没有获取到锁，重试
                Thread.sleep(200);
                tryLock(lockName, uuid, expire);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 锁续期
        this.renewTime(lockName, uuid, expire);

        // 获取到锁，返回true
        return true;
    }


    private void unlock(String lockName, String uuid){
        String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
                "    return nil;" +
                "end;" +
                "if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
                "    return 0;" +
                "else" +
                "    redis.call('del', KEYS[1]);" +
                "    return 1;" +
                "end;";
        // 这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中，三个返回值含义如下：
        // 1 代表解锁成功，锁被释放
        // 0 代表可重入次数被减 1
        // null 代表其他线程尝试解锁，解锁失败
        Long result = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        // 如果未返回值，代表尝试解其他线程的锁
        if (result == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: "
                    + lockName + " with request: "  + uuid);
        }
    }



    /**
     * 锁延期。看门狗机制。
     * 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,因此在此过程中,其他线程无法获取到锁,保证了线程安全性
     * @param lockName
     * @param expire 单位：毫秒
     */
    private void renewTime(String lockName, String uuid, Long expire){
        String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
        new Thread(() -> {
            while (this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
                try {
                   log.info("分布式锁执行了续期。。。。");
                    // 到达过期时间的2/3时间，自动续期
                    Thread.sleep(expire*1000 / 3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }





    //以下测试本地锁的环境
//    @Autowired
//    private redisTemplate redisTemplate;

    /**
     * 没加synchronized同步锁，并发执行方法，执行++num出现资源访问并发问题。  ++num  不是原子的。
     *
     * 解决办法：
     *      1.原子整型类： AtomicInteger
     *      2.synchronized或Lock
     *
     *  本地锁：只能应用在单实例环境（同一个JVM中）下有效。
     *
     *  在集群实例环境下，多个JVM环境。这时本地锁就失效了。所以，需要使用分布式锁来解决数据安全问题。
     */
/*    @Override
    public synchronized void testLock() {
        // 查询Redis中的num值
        String value = (String)this.stringRedisTemplate.opsForValue().get("num");
        // 没有该值return
        if (StringUtils.isBlank(value)){
            return ;
        }
        // 有值就转成成int
        int num = Integer.parseInt(value);
        // 把Redis中的num值+1
        this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    }*/
}
